TechNote - Window Focus
Thursday, June 4, 2026
7:21 AM
OneMore dialogs (search, etc.) may open without receiving keyboard focus — the focus stays on the OneNote page. This happens because Windows enforces strict foreground window security: SetForegroundWindow() silently fails unless the calling thread currently holds "foreground rights."
OneMore runs as a COM surrogate (dllhost.exe). Both trigger paths lose foreground rights before the dialog appears:
- Ribbon path: SearchCmd is async Task — it returns the task to the ribbon framework immediately, and the dialog is created on a ThreadPool continuation thread that has no foreground rights.
- Hotkey path: key.Action() is invoked from WndProc (which does have rights via WM_HOTKEY), but the async method suspends at the first await and continues on a ThreadPool thread — rights are lost.
The current Elevate() method uses a TopMost toggle trick and calls Select()/Focus(), but these don't actually transfer focus from another process. SetForegroundWindow is commented out because it fails silently without rights.
Solution: AttachThreadInput
The well-established technique is to temporarily attach the input queue of our dialog thread to the foreground thread (OneNote), which grants our thread the ability to call SetForegroundWindow successfully:
uint foregroundThread = Native.GetWindowThreadProcessId(foreground, out _);
uint currentThread = Native.GetCurrentThreadId();
Native.AttachThreadInput(foregroundThread, currentThread, true); // attach
Native.SetForegroundWindow(Handle);
Native.BringWindowToTop(Handle);
Native.AttachThreadInput(foregroundThread, currentThread, false); // detach immediately
The detach is immediate — the threads share the input queue only for the two API calls, which avoids any side effects on modal loops or message routing.
Files to Change
1. OneMore/Helpers/Native.cs
Add three P/Invoke declarations alongside the existing SetForegroundWindow / GetWindowThreadProcessId block:
[DllImport("kernel32.dll")]
public static extern uint GetCurrentThreadId();
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool BringWindowToTop(IntPtr hWnd);
2. OneMore/UI/MoreForm.cs — Elevate() method
Replace the comment-out //Native.SetForegroundWindow(Handle); and the TopMost-only approach with:
public void Elevate(bool keepTop = true)
{
if (DesignMode) return;
if (modeless) BringToFront();
// Temporarily share input queue with the foreground thread so
// SetForegroundWindow succeeds regardless of foreground rights.
var foreground = Native.GetForegroundWindow();
if (foreground != IntPtr.Zero && foreground != Handle)
{
uint foregroundThread = Native.GetWindowThreadProcessId(foreground, out _);
uint currentThread = Native.GetCurrentThreadId();
bool attached = foregroundThread != currentThread &&
Native.AttachThreadInput(foregroundThread, currentThread, true);
Native.SetForegroundWindow(Handle);
Native.BringWindowToTop(Handle);
if (attached)
{
Native.AttachThreadInput(foregroundThread, currentThread, false);
}
}
TopMost = false;
TopMost = true;
TopMost = keepTop;
if (!IsDisposed)
{
try { Select(); Focus(); }
catch { /* swallow disposed exception */ }
}
}
No changes to HotkeyManager, SearchCommand, WindowElevator, or any individual dialog.
#omwiki #omdeveloper #omtechnote
© 2020 Steven M Cohn. All rights reserved.
Please consider a sponsorship or one-time donation to support ongoing development
Created with OneNote.